@kernel.chat/kbot 3.97.0 → 3.97.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/cli.js +1 -1
- package/dist/coordinator.d.ts +32 -0
- package/dist/coordinator.js +157 -0
- package/dist/doctor.js +5 -4
- package/dist/share.js +1 -1
- package/dist/streaming.js +1 -1
- package/dist/tools/ableton.js +176 -42
- package/dist/tools/audit.js +2 -2
- package/dist/tools/containers.js +75 -14
- package/dist/tools/estimation.d.ts +2 -0
- package/dist/tools/estimation.js +21 -0
- package/dist/tools/idempotency-checker.d.ts +2 -0
- package/dist/tools/idempotency-checker.js +23 -0
- package/dist/tools/image-variation.d.ts +2 -0
- package/dist/tools/image-variation.js +31 -0
- package/dist/tools/index.js +1 -0
- package/dist/tools/one-prompt-producer.d.ts +2 -0
- package/dist/tools/one-prompt-producer.js +723 -0
- package/dist/tools/sound-designer.js +278 -3
- package/dist/tools/stream-brain.js +1 -1
- package/dist/tools/stream-character.js +4 -4
- package/dist/tools/stream-intelligence.js +2 -2
- package/dist/tools/stream-renderer.js +30 -30
- package/dist/ui.js +23 -21
- package/dist/watcher.d.ts +16 -0
- package/dist/watcher.js +111 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
<strong>kbot</strong><br>
|
|
5
|
-
Open-source terminal AI agent.
|
|
5
|
+
Open-source terminal AI agent. 787+ tools. 35 agents. 20 providers. Dreams, learns, watches your system, controls your phone. $0 local.
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
9
|
-
<img src="https://raw.githubusercontent.com/isaacsight/kernel/main/tools/video-assets/demo.gif" alt="kbot demo" width="700">
|
|
9
|
+
<img src="https://raw.githubusercontent.com/isaacsight/kernel/main/tools/video-assets/demo-quick.gif" alt="kbot demo" width="700">
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
@@ -31,7 +31,7 @@ Most terminal AI agents lock you into one provider, one model, one way of workin
|
|
|
31
31
|
- **Runs fully offline** — Embedded llama.cpp, Ollama, LM Studio, or Jan. $0, fully private.
|
|
32
32
|
- **Learns your patterns** — Bayesian skill ratings + pattern extraction. Gets faster over time.
|
|
33
33
|
- **35 specialist agents** — auto-routes your request to the right expert (coder, researcher, writer, guardian, quant, and 30 more).
|
|
34
|
-
- **
|
|
34
|
+
- **787+ tools** — files, bash, git, GitHub, web search, deploy, database, game dev, VFX, research, science, finance, security, music production, iPhone control, and more.
|
|
35
35
|
- **Programmatic SDK** — use kbot as a library in your own apps.
|
|
36
36
|
- **MCP server built in** — plug kbot into Claude Code, Cursor, VS Code, Zed, or Neovim as a tool provider.
|
|
37
37
|
|
|
@@ -142,7 +142,7 @@ Checks security, documentation, code quality, CI/CD, community health, and DevOp
|
|
|
142
142
|
|---|---|---|---|---|---|
|
|
143
143
|
| AI providers | 20 | 1 | 1 | 6 | 75+ |
|
|
144
144
|
| Specialist agents | 35 | 0 | 0 | 0 | 0 |
|
|
145
|
-
| Built-in tools |
|
|
145
|
+
| Built-in tools | 787+ | ~20 | ~15 | ~10 | ~15 |
|
|
146
146
|
| Science tools | 114 | 0 | 0 | 0 | 0 |
|
|
147
147
|
| Memory system | 7-tier bidirectional | File-based | No | No | No |
|
|
148
148
|
| Dream engine | Yes ($0 local) | Cloud API | No | No | No |
|
package/dist/cli.js
CHANGED
|
@@ -102,7 +102,7 @@ async function main() {
|
|
|
102
102
|
console.log(` ${chalk.cyan('https://github.com/isaacsight/kernel/issues')} ${chalk.dim('Bug reports')}`);
|
|
103
103
|
console.log(` ${chalk.cyan('support@kernel.chat')} ${chalk.dim('Email (AI-assisted replies)')}`);
|
|
104
104
|
console.log();
|
|
105
|
-
console.log(` ${chalk.dim('35 specialist agents ·
|
|
105
|
+
console.log(` ${chalk.dim('35 specialist agents · 787+ tools · 20 providers · MIT licensed')}`);
|
|
106
106
|
console.log();
|
|
107
107
|
process.exit(0);
|
|
108
108
|
});
|
package/dist/coordinator.d.ts
CHANGED
|
@@ -129,4 +129,36 @@ export declare class IntelligenceCoordinator {
|
|
|
129
129
|
export declare function getCoordinator(): IntelligenceCoordinator;
|
|
130
130
|
/** Register coordinator tools with the kbot tool registry */
|
|
131
131
|
export declare function registerCoordinatorTools(): void;
|
|
132
|
+
export interface Task {
|
|
133
|
+
id: string;
|
|
134
|
+
goal: string;
|
|
135
|
+
status: 'pending' | 'running' | 'done' | 'failed';
|
|
136
|
+
agent: string;
|
|
137
|
+
dependencies: string[];
|
|
138
|
+
result?: string;
|
|
139
|
+
error?: string;
|
|
140
|
+
}
|
|
141
|
+
export interface CoordinatorPlan {
|
|
142
|
+
id: string;
|
|
143
|
+
goal: string;
|
|
144
|
+
tasks: Task[];
|
|
145
|
+
createdAt: string;
|
|
146
|
+
completedAt?: string;
|
|
147
|
+
status: 'planning' | 'executing' | 'done' | 'failed';
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Decompose a high-level goal into ordered sub-tasks with dependencies.
|
|
151
|
+
* Uses the LLM (via runAgent) to break the goal into 2-6 concrete tasks.
|
|
152
|
+
*/
|
|
153
|
+
export declare function decompose(goal: string): Promise<CoordinatorPlan>;
|
|
154
|
+
/**
|
|
155
|
+
* Execute a plan's tasks in dependency order (sequential for now).
|
|
156
|
+
* Tasks whose dependencies are all 'done' are eligible to run.
|
|
157
|
+
*/
|
|
158
|
+
export declare function execute(plan: CoordinatorPlan): Promise<CoordinatorPlan>;
|
|
159
|
+
/**
|
|
160
|
+
* Convenience: decompose a goal, execute the plan, and synthesize results.
|
|
161
|
+
* Returns a human-readable summary of what was accomplished.
|
|
162
|
+
*/
|
|
163
|
+
export declare function coordinate(goal: string): Promise<string>;
|
|
132
164
|
//# sourceMappingURL=coordinator.d.ts.map
|
package/dist/coordinator.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
11
|
+
import chalk from 'chalk';
|
|
11
12
|
// ── Lazy module loaders (dynamic imports to break circular deps) ──
|
|
12
13
|
async function getLearning() { return import('./learning.js'); }
|
|
13
14
|
async function getConfidence() { return import('./confidence.js'); }
|
|
@@ -673,10 +674,166 @@ export function registerCoordinatorTools() {
|
|
|
673
674
|
return JSON.stringify(result, null, 2);
|
|
674
675
|
},
|
|
675
676
|
});
|
|
677
|
+
registerTool({
|
|
678
|
+
name: 'coordinator_orchestrate',
|
|
679
|
+
description: 'Decompose a goal into sub-tasks, assign specialist agents, execute in dependency order, and synthesize results',
|
|
680
|
+
parameters: { goal: { type: 'string', description: 'The high-level goal to orchestrate', required: true } },
|
|
681
|
+
tier: 'free',
|
|
682
|
+
execute: async (args) => coordinate(args.goal),
|
|
683
|
+
});
|
|
676
684
|
}).catch(() => {
|
|
677
685
|
// tools/index.js not available — skip registration
|
|
678
686
|
});
|
|
679
687
|
}
|
|
688
|
+
// ── Goal Decomposition & Multi-Agent Orchestration ──
|
|
689
|
+
//
|
|
690
|
+
// Takes a high-level goal and:
|
|
691
|
+
// 1. Decomposes it into ordered sub-tasks via LLM
|
|
692
|
+
// 2. Assigns each sub-task to the best specialist agent
|
|
693
|
+
// 3. Manages dependencies between sub-tasks
|
|
694
|
+
// 4. Synthesizes results into a final output
|
|
695
|
+
// 5. Learns from the execution for next time
|
|
696
|
+
const COORD_AMETHYST = typeof chalk.hex === 'function' ? chalk.hex('#6B5B95') : ((s) => s);
|
|
697
|
+
const DECOMPOSE_PROMPT = `You are a task decomposition engine. Break the goal into 2-6 concrete sub-tasks.
|
|
698
|
+
Output ONLY valid JSON: {"tasks":[{"id":"t1","goal":"...","agent":"agent_id","dependencies":[]}]}
|
|
699
|
+
Agents: kernel (general), coder (code/debug), researcher (research), writer (docs), analyst (strategy), guardian (security), infrastructure (devops).
|
|
700
|
+
Rules: each task independently verifiable, use dependencies for ordering, assign best agent, one objective per task.`;
|
|
701
|
+
/**
|
|
702
|
+
* Decompose a high-level goal into ordered sub-tasks with dependencies.
|
|
703
|
+
* Uses the LLM (via runAgent) to break the goal into 2-6 concrete tasks.
|
|
704
|
+
*/
|
|
705
|
+
export async function decompose(goal) {
|
|
706
|
+
const planId = shortId();
|
|
707
|
+
const plan = {
|
|
708
|
+
id: planId,
|
|
709
|
+
goal,
|
|
710
|
+
tasks: [],
|
|
711
|
+
createdAt: new Date().toISOString(),
|
|
712
|
+
status: 'planning',
|
|
713
|
+
};
|
|
714
|
+
process.stderr.write(` ${COORD_AMETHYST('◆ decompose')} ${chalk.dim(goal.slice(0, 80))}${goal.length > 80 ? '...' : ''}\n`);
|
|
715
|
+
try {
|
|
716
|
+
// Lazy import to avoid circular dependency
|
|
717
|
+
const { runAgent } = await import('./agent.js');
|
|
718
|
+
const response = await runAgent(`${DECOMPOSE_PROMPT}\n\nGoal: ${goal}\n\nOutput JSON:`, { agent: 'kernel', skipPlanner: true, sessionId: `coord-${planId}` });
|
|
719
|
+
// Parse JSON from response
|
|
720
|
+
const jsonMatch = response.content.match(/\{[\s\S]*\}/);
|
|
721
|
+
if (jsonMatch) {
|
|
722
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
723
|
+
plan.tasks = (parsed.tasks || []).slice(0, 6).map(t => ({
|
|
724
|
+
id: t.id || shortId(),
|
|
725
|
+
goal: t.goal,
|
|
726
|
+
status: 'pending',
|
|
727
|
+
agent: t.agent || 'kernel',
|
|
728
|
+
dependencies: t.dependencies || [],
|
|
729
|
+
}));
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
catch (err) {
|
|
733
|
+
process.stderr.write(` ${chalk.red('✗')} decomposition failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
734
|
+
}
|
|
735
|
+
// Fallback: if decomposition produced nothing, create a single task
|
|
736
|
+
if (plan.tasks.length === 0) {
|
|
737
|
+
plan.tasks = [{
|
|
738
|
+
id: 't1',
|
|
739
|
+
goal,
|
|
740
|
+
status: 'pending',
|
|
741
|
+
agent: 'kernel',
|
|
742
|
+
dependencies: [],
|
|
743
|
+
}];
|
|
744
|
+
}
|
|
745
|
+
plan.status = 'executing';
|
|
746
|
+
for (const task of plan.tasks) {
|
|
747
|
+
const deps = task.dependencies.length > 0 ? chalk.dim(` -> ${task.dependencies.join(',')}`) : '';
|
|
748
|
+
process.stderr.write(` ${chalk.dim('│')} ${chalk.cyan(task.id)} ${task.goal} ${chalk.magenta(`@${task.agent}`)}${deps}\n`);
|
|
749
|
+
}
|
|
750
|
+
return plan;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Execute a plan's tasks in dependency order (sequential for now).
|
|
754
|
+
* Tasks whose dependencies are all 'done' are eligible to run.
|
|
755
|
+
*/
|
|
756
|
+
export async function execute(plan) {
|
|
757
|
+
const { runAgent } = await import('./agent.js');
|
|
758
|
+
const completed = new Set();
|
|
759
|
+
process.stderr.write(`\n ${COORD_AMETHYST('◆ execute')} ${plan.tasks.length} tasks\n`);
|
|
760
|
+
while (completed.size < plan.tasks.length) {
|
|
761
|
+
// Find tasks whose dependencies are all satisfied
|
|
762
|
+
const ready = plan.tasks.filter(t => t.status === 'pending' &&
|
|
763
|
+
t.dependencies.every(dep => completed.has(dep)));
|
|
764
|
+
if (ready.length === 0) {
|
|
765
|
+
// Remaining tasks have unmet deps — mark them failed
|
|
766
|
+
for (const t of plan.tasks) {
|
|
767
|
+
if (t.status === 'pending') {
|
|
768
|
+
t.status = 'failed';
|
|
769
|
+
t.error = 'Unmet dependencies (earlier tasks failed)';
|
|
770
|
+
completed.add(t.id);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
// Execute ready tasks sequentially (parallel execution can come later)
|
|
776
|
+
for (const task of ready) {
|
|
777
|
+
task.status = 'running';
|
|
778
|
+
process.stderr.write(` ${chalk.dim('├')} ${chalk.yellow('●')} ${task.id}: ${task.goal}\n`);
|
|
779
|
+
// Gather dependency results as context
|
|
780
|
+
const depCtx = task.dependencies
|
|
781
|
+
.map(id => { const d = plan.tasks.find(t => t.id === id); return d?.result ? `[${id}]: ${d.result.slice(0, 500)}` : ''; })
|
|
782
|
+
.filter(Boolean).join('\n');
|
|
783
|
+
const prompt = `You are executing a sub-task of a larger plan.${depCtx ? `\n\nPrevious results:\n${depCtx}` : ''}\n\nYour task: ${task.goal}\n\nExecute this now.`;
|
|
784
|
+
try {
|
|
785
|
+
const response = await runAgent(prompt, {
|
|
786
|
+
agent: task.agent,
|
|
787
|
+
skipPlanner: true,
|
|
788
|
+
sessionId: `coord-${plan.id}-${task.id}`,
|
|
789
|
+
});
|
|
790
|
+
task.result = response.content;
|
|
791
|
+
task.status = 'done';
|
|
792
|
+
process.stderr.write(` ${chalk.dim('│')} ${chalk.green('✓')} ${task.id} done\n`);
|
|
793
|
+
}
|
|
794
|
+
catch (err) {
|
|
795
|
+
task.status = 'failed';
|
|
796
|
+
task.error = err instanceof Error ? err.message : String(err);
|
|
797
|
+
process.stderr.write(` ${chalk.dim('│')} ${chalk.red('✗')} ${task.id} failed: ${task.error}\n`);
|
|
798
|
+
}
|
|
799
|
+
completed.add(task.id);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
// Determine final plan status
|
|
803
|
+
const failed = plan.tasks.filter(t => t.status === 'failed');
|
|
804
|
+
plan.status = failed.length === 0 ? 'done' : 'failed';
|
|
805
|
+
plan.completedAt = new Date().toISOString();
|
|
806
|
+
const done = plan.tasks.filter(t => t.status === 'done').length;
|
|
807
|
+
process.stderr.write(` ${chalk.dim('└')} ${done}/${plan.tasks.length} tasks succeeded\n`);
|
|
808
|
+
// Record outcome for learning
|
|
809
|
+
try {
|
|
810
|
+
const coord = getCoordinator();
|
|
811
|
+
if (plan.status === 'done')
|
|
812
|
+
coord.addGoal(plan.goal, 0.7).status = 'completed';
|
|
813
|
+
const learning = await getLearning();
|
|
814
|
+
learning.learnFromExchange(`[coordinator] ${plan.goal}`, `${done}/${plan.tasks.length} done`, plan.tasks.map(t => t.agent));
|
|
815
|
+
}
|
|
816
|
+
catch { /* non-critical */ }
|
|
817
|
+
return plan;
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Convenience: decompose a goal, execute the plan, and synthesize results.
|
|
821
|
+
* Returns a human-readable summary of what was accomplished.
|
|
822
|
+
*/
|
|
823
|
+
export async function coordinate(goal) {
|
|
824
|
+
const plan = await decompose(goal);
|
|
825
|
+
const executed = await execute(plan);
|
|
826
|
+
// Synthesize results
|
|
827
|
+
const results = executed.tasks
|
|
828
|
+
.filter(t => t.status === 'done' && t.result)
|
|
829
|
+
.map(t => `## ${t.goal}\n${t.result}`)
|
|
830
|
+
.join('\n\n');
|
|
831
|
+
const failed = executed.tasks.filter(t => t.status === 'failed');
|
|
832
|
+
const failSummary = failed.length > 0
|
|
833
|
+
? `\n\n---\n${failed.length} task(s) failed:\n${failed.map(t => `- ${t.goal}: ${t.error}`).join('\n')}`
|
|
834
|
+
: '';
|
|
835
|
+
return results + failSummary;
|
|
836
|
+
}
|
|
680
837
|
// Auto-register tools when this module is imported
|
|
681
838
|
registerCoordinatorTools();
|
|
682
839
|
//# sourceMappingURL=coordinator.js.map
|
package/dist/doctor.js
CHANGED
|
@@ -518,11 +518,12 @@ export async function runDoctor() {
|
|
|
518
518
|
// ── Formatter ──
|
|
519
519
|
// Color palette matching ui.ts
|
|
520
520
|
const useColor = !process.env.NO_COLOR && process.stdout.isTTY !== false;
|
|
521
|
-
const
|
|
522
|
-
const
|
|
523
|
-
const
|
|
521
|
+
const hex = typeof chalk.hex === 'function' ? (c) => chalk.hex(c) : () => ((s) => s);
|
|
522
|
+
const GREEN = useColor ? hex('#4ADE80') : ((s) => s);
|
|
523
|
+
const RED = useColor ? hex('#F87171') : ((s) => s);
|
|
524
|
+
const YELLOW = useColor ? hex('#FBBF24') : ((s) => s);
|
|
524
525
|
const DIM = useColor ? chalk.dim : ((s) => s);
|
|
525
|
-
const ACCENT = useColor ?
|
|
526
|
+
const ACCENT = useColor ? hex('#A78BFA') : ((s) => s);
|
|
526
527
|
const STATUS_ICON = {
|
|
527
528
|
pass: GREEN('✓'),
|
|
528
529
|
warn: YELLOW('!'),
|
package/dist/share.js
CHANGED
|
@@ -25,7 +25,7 @@ export function formatShareMarkdown(turns, meta) {
|
|
|
25
25
|
const lines = [
|
|
26
26
|
`# ${name}`,
|
|
27
27
|
'',
|
|
28
|
-
`> Generated with [kbot](${KBOT_URL}) —
|
|
28
|
+
`> Generated with [kbot](${KBOT_URL}) — 35 specialist agents, 787+ tools, 20 AI providers`,
|
|
29
29
|
`> Agent: \`${agent}\` | Date: ${date}`,
|
|
30
30
|
'',
|
|
31
31
|
'---',
|
package/dist/streaming.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// The thinking display shows the AI's reasoning process in real-time,
|
|
8
8
|
// then the final response renders normally.
|
|
9
9
|
import chalk from 'chalk';
|
|
10
|
-
const ACCENT_DIM = chalk.hex('#7C6CB0');
|
|
10
|
+
const ACCENT_DIM = typeof chalk.hex === 'function' ? chalk.hex('#7C6CB0') : chalk.dim;
|
|
11
11
|
const THINKING_COLOR = chalk.dim.italic;
|
|
12
12
|
/** Max accumulated content size during streaming (5MB) to prevent OOM */
|
|
13
13
|
const MAX_STREAM_CONTENT = 5 * 1024 * 1024;
|
package/dist/tools/ableton.js
CHANGED
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
// ableton_mixer — snapshot levels, batch set, sends
|
|
12
12
|
// ableton_create_progression — chord progressions → MIDI in clips
|
|
13
13
|
// ableton_session_info — full session state snapshot
|
|
14
|
+
// ableton_audio_analysis — real-time audio level meters (track + master RMS)
|
|
14
15
|
// ableton_knowledge — deep Ableton knowledge base queries (registered in ableton-knowledge.ts)
|
|
15
16
|
//
|
|
16
17
|
// Requires: AbletonOSC loaded in Ableton Live (Preferences → Link/Tempo/MIDI → Control Surface)
|
|
17
18
|
import { registerTool } from './index.js';
|
|
19
|
+
import { execSync } from 'node:child_process';
|
|
18
20
|
import { ensureAbleton, formatAbletonError } from '../integrations/ableton-osc.js';
|
|
19
21
|
import { parseProgression, voiceChord, arpeggiate, NAMED_PROGRESSIONS, RHYTHM_PATTERNS, GENRE_DRUM_PATTERNS, noteNameToMidi, midiToNoteName, } from './music-theory.js';
|
|
20
22
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
@@ -769,52 +771,89 @@ export function registerAbletonTools() {
|
|
|
769
771
|
// ─── 10. Load Plugin ──────────────────────────────────────────────────
|
|
770
772
|
registerTool({
|
|
771
773
|
name: 'ableton_load_plugin',
|
|
772
|
-
description: 'Load any instrument or plugin onto a track by name — native (Operator, Wavetable, Drift) or third-party VST/AU (Serum 2, Vital, Kontakt).
|
|
774
|
+
description: 'Load any instrument or plugin onto a track by name — native (Operator, Wavetable, Drift) or third-party VST/AU (Serum 2, Vital, Kontakt). Tries OSC first, then falls back to AppleScript browser automation on macOS.',
|
|
773
775
|
parameters: {
|
|
774
776
|
track: { type: 'number', description: 'Track number (1-based)', required: true },
|
|
775
777
|
plugin: { type: 'string', description: 'Plugin name to search for (e.g. "Serum 2", "Operator", "Wavetable")', required: true },
|
|
776
778
|
manufacturer: { type: 'string', description: 'Manufacturer name for VST/AU (e.g. "Xfer Records", "Native Instruments"). Optional — helps narrow search.' },
|
|
779
|
+
skip_results: { type: 'number', description: 'Number of Down arrow presses before selecting (to skip past FX/presets). Default: 1' },
|
|
777
780
|
},
|
|
778
781
|
tier: 'free',
|
|
779
|
-
timeout:
|
|
782
|
+
timeout: 20_000,
|
|
780
783
|
async execute(args) {
|
|
781
784
|
const t = userTrack(args.track);
|
|
782
785
|
const plugin = String(args.plugin);
|
|
783
786
|
const manufacturer = args.manufacturer ? String(args.manufacturer) : '';
|
|
787
|
+
const skipResults = Number(args.skip_results) || 1;
|
|
788
|
+
// ── Attempt 1: OSC (fast, reliable for native instruments) ──
|
|
784
789
|
try {
|
|
785
790
|
const osc = await ensureAbleton();
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
// Try load_plugin first — _deep_plugin_search correctly handles native
|
|
795
|
-
// instruments by returning the first loadable child of matching browser folders.
|
|
796
|
-
// This works for both native (Drum Rack, Operator, Wavetable) and third-party plugins.
|
|
797
|
-
let result = await osc.query('/live/kbot/load_plugin', t, plugin, '');
|
|
798
|
-
let status = extractArgs(result);
|
|
799
|
-
if (status[0] === 'ok') {
|
|
800
|
-
return `Loaded **${status[1]}** on track ${args.track}`;
|
|
801
|
-
}
|
|
802
|
-
// Fallback: try load_device (uses _search_tree, less reliable for native instruments)
|
|
803
|
-
result = await osc.query('/live/kbot/load_device', t, plugin);
|
|
804
|
-
status = extractArgs(result);
|
|
805
|
-
if (status[0] === 'ok') {
|
|
806
|
-
return `Loaded **${status[1]}** on track ${args.track}`;
|
|
791
|
+
// Select the target track first so the plugin loads there
|
|
792
|
+
osc.send('/live/song/set/current_track', t);
|
|
793
|
+
await new Promise(r => setTimeout(r, 200));
|
|
794
|
+
// Try native load_device endpoint (works for Ableton built-in instruments)
|
|
795
|
+
const nativeResult = await osc.query('/live/track/load/device', t, plugin);
|
|
796
|
+
const nativeStatus = extractArgs(nativeResult);
|
|
797
|
+
if (nativeStatus.length > 0 && String(nativeStatus[0]) !== 'error') {
|
|
798
|
+
return `Loaded **${plugin}** on track ${args.track} (via OSC)`;
|
|
807
799
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
800
|
+
}
|
|
801
|
+
catch {
|
|
802
|
+
// OSC failed — fall through to AppleScript
|
|
803
|
+
}
|
|
804
|
+
// ── Attempt 2: AppleScript browser automation (macOS only) ──
|
|
805
|
+
// Uses Ableton's built-in browser search: Cmd+F → type name → arrow down → Return
|
|
806
|
+
// Proven approach from ZENOLOGY loading session
|
|
807
|
+
if (process.platform !== 'darwin') {
|
|
808
|
+
return `Plugin "${plugin}" could not be loaded via OSC. AppleScript fallback is macOS-only.`;
|
|
809
|
+
}
|
|
810
|
+
try {
|
|
811
|
+
const searchTerm = manufacturer ? `${manufacturer} ${plugin}` : plugin;
|
|
812
|
+
// Build AppleScript: activate Ableton, open browser search, type plugin name, select, load
|
|
813
|
+
const downArrows = Array(skipResults).fill('key code 125').join('\ndelay 0.3\n'); // 125 = Down arrow
|
|
814
|
+
const script = `
|
|
815
|
+
tell application "Ableton Live 12"
|
|
816
|
+
activate
|
|
817
|
+
end tell
|
|
818
|
+
delay 0.5
|
|
819
|
+
tell application "System Events"
|
|
820
|
+
tell process "Ableton Live 12"
|
|
821
|
+
-- Open browser search: Cmd+F (View > Search in Browser)
|
|
822
|
+
keystroke "f" using command down
|
|
823
|
+
delay 0.8
|
|
824
|
+
-- Clear any existing search text and type plugin name
|
|
825
|
+
keystroke "a" using command down
|
|
826
|
+
delay 0.1
|
|
827
|
+
keystroke "${searchTerm.replace(/"/g, '\\"')}"
|
|
828
|
+
delay 1.0
|
|
829
|
+
-- Navigate down to the result (skip past categories/folders)
|
|
830
|
+
${downArrows}
|
|
831
|
+
delay 0.3
|
|
832
|
+
-- Press Return to load the selected item onto the current track
|
|
833
|
+
key code 36
|
|
834
|
+
delay 0.5
|
|
835
|
+
end tell
|
|
836
|
+
end tell
|
|
837
|
+
return "ok"
|
|
838
|
+
`;
|
|
839
|
+
const lines = script.split('\n').filter(l => l.trim());
|
|
840
|
+
const escapedArgs = lines.map(l => `-e '${l.replace(/'/g, "'\\''")}'`).join(' ');
|
|
841
|
+
const result = execSync(`osascript ${escapedArgs}`, {
|
|
842
|
+
encoding: 'utf-8',
|
|
843
|
+
timeout: 15_000,
|
|
844
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
845
|
+
}).trim();
|
|
846
|
+
if (result === 'ok') {
|
|
847
|
+
return `Loaded **${plugin}** on track ${args.track} (via AppleScript browser search)`;
|
|
813
848
|
}
|
|
814
|
-
return `
|
|
849
|
+
return `AppleScript returned unexpected result: ${result}`;
|
|
815
850
|
}
|
|
816
851
|
catch (err) {
|
|
817
|
-
|
|
852
|
+
const msg = err.message;
|
|
853
|
+
if (msg.includes('not allowed') || msg.includes('assistive')) {
|
|
854
|
+
return `AppleScript failed — Accessibility permission required.\n\nGrant permission in System Settings > Privacy & Security > Accessibility for your terminal app.`;
|
|
855
|
+
}
|
|
856
|
+
return `Failed to load "${plugin}": OSC endpoint not available, AppleScript fallback failed.\n\nError: ${msg}`;
|
|
818
857
|
}
|
|
819
858
|
},
|
|
820
859
|
});
|
|
@@ -964,21 +1003,19 @@ export function registerAbletonTools() {
|
|
|
964
1003
|
if (args.name) {
|
|
965
1004
|
osc.send('/live/track/set/name', newTrackIdx, String(args.name));
|
|
966
1005
|
}
|
|
967
|
-
// Load instrument if specified —
|
|
1006
|
+
// Load instrument if specified — try native OSC first, then load_plugin tool handles fallbacks
|
|
968
1007
|
if (args.instrument) {
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
const result = await osc.query('/live/kbot/load_plugin', newTrackIdx, String(args.instrument), '');
|
|
976
|
-
const status = extractArgs(result);
|
|
977
|
-
if (status[0] !== 'ok') {
|
|
978
|
-
// Fallback to load_device
|
|
979
|
-
await osc.query('/live/kbot/load_device', newTrackIdx, String(args.instrument));
|
|
1008
|
+
try {
|
|
1009
|
+
const loadResult = await osc.query('/live/track/load/device', newTrackIdx, String(args.instrument));
|
|
1010
|
+
const loadStatus = extractArgs(loadResult);
|
|
1011
|
+
if (loadStatus.length === 0 || String(loadStatus[0]) === 'error') {
|
|
1012
|
+
// Native load failed — OSC doesn't have this device. The user can use
|
|
1013
|
+
// ableton_load_plugin separately which has AppleScript fallback.
|
|
980
1014
|
}
|
|
981
1015
|
}
|
|
1016
|
+
catch {
|
|
1017
|
+
// Timeout or connection error — device may still have loaded, continue
|
|
1018
|
+
}
|
|
982
1019
|
}
|
|
983
1020
|
return `Created ${trackType} track **${args.name || 'Track ' + (newTrackIdx + 1)}**${args.instrument ? ' with ' + args.instrument : ''} (track ${newTrackIdx + 1})`;
|
|
984
1021
|
}
|
|
@@ -1035,5 +1072,102 @@ export function registerAbletonTools() {
|
|
|
1035
1072
|
}
|
|
1036
1073
|
},
|
|
1037
1074
|
});
|
|
1075
|
+
// ─── 15. Audio Analysis ──────────────────────────────────────────────
|
|
1076
|
+
registerTool({
|
|
1077
|
+
name: 'ableton_audio_analysis',
|
|
1078
|
+
description: 'Get real-time audio level meters from Ableton Live — track output levels (L/R RMS), master output, and peak detection. Use this to hear what is playing, check if a track has signal, or monitor the mix.',
|
|
1079
|
+
parameters: {
|
|
1080
|
+
track: { type: 'number', description: 'Track number to analyze (1-based). Omit to get master output only.' },
|
|
1081
|
+
all_tracks: { type: 'boolean', description: 'If true, read levels for all tracks plus master. Default: false' },
|
|
1082
|
+
},
|
|
1083
|
+
tier: 'free',
|
|
1084
|
+
timeout: 10_000,
|
|
1085
|
+
async execute(args) {
|
|
1086
|
+
try {
|
|
1087
|
+
const osc = await ensureAbleton();
|
|
1088
|
+
const lines = ['## Audio Levels', ''];
|
|
1089
|
+
// Helper to read a meter value with error handling
|
|
1090
|
+
async function readMeter(address, ...oscArgs) {
|
|
1091
|
+
try {
|
|
1092
|
+
const result = await osc.query(address, ...oscArgs);
|
|
1093
|
+
const vals = extractArgs(result);
|
|
1094
|
+
// Meter values are typically floats 0.0 - 1.0 (or higher for clipping)
|
|
1095
|
+
return typeof vals[vals.length - 1] === 'number' ? vals[vals.length - 1] : 0;
|
|
1096
|
+
}
|
|
1097
|
+
catch {
|
|
1098
|
+
return -1; // timeout = no response
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
// Meter bar visualization
|
|
1102
|
+
function meterBar(level) {
|
|
1103
|
+
if (level < 0)
|
|
1104
|
+
return '[-no signal-]';
|
|
1105
|
+
const db = level > 0 ? 20 * Math.log10(level) : -Infinity;
|
|
1106
|
+
const dbStr = db === -Infinity ? '-inf' : db.toFixed(1);
|
|
1107
|
+
const barLen = Math.min(20, Math.max(0, Math.round(level * 20)));
|
|
1108
|
+
const bar = '\u2588'.repeat(barLen) + '\u2591'.repeat(20 - barLen);
|
|
1109
|
+
return `[${bar}] ${dbStr} dB`;
|
|
1110
|
+
}
|
|
1111
|
+
if (args.all_tracks) {
|
|
1112
|
+
// Read all track levels
|
|
1113
|
+
const countResult = await osc.query('/live/song/get/num_tracks');
|
|
1114
|
+
const numTracks = Number(extractArgs(countResult)[0]) || 0;
|
|
1115
|
+
for (let t = 0; t < numTracks - 1; t++) { // -1 to skip master return
|
|
1116
|
+
const nameResult = await osc.query('/live/track/get/name', t);
|
|
1117
|
+
const name = extractArgs(nameResult)[1] || `Track ${t + 1}`;
|
|
1118
|
+
const left = await readMeter('/live/track/get/output_meter_left', t);
|
|
1119
|
+
const right = await readMeter('/live/track/get/output_meter_right', t);
|
|
1120
|
+
const avg = left >= 0 && right >= 0 ? (left + right) / 2 : Math.max(left, right);
|
|
1121
|
+
lines.push(`**${displayTrack(t)}. ${name}**: ${meterBar(avg)}`);
|
|
1122
|
+
if (left >= 0 && right >= 0 && Math.abs(left - right) > 0.05) {
|
|
1123
|
+
lines.push(` L: ${meterBar(left)} | R: ${meterBar(right)}`);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
lines.push('');
|
|
1127
|
+
}
|
|
1128
|
+
else if (args.track) {
|
|
1129
|
+
// Read specific track
|
|
1130
|
+
const t = userTrack(args.track);
|
|
1131
|
+
const nameResult = await osc.query('/live/track/get/name', t);
|
|
1132
|
+
const name = extractArgs(nameResult)[1] || `Track ${args.track}`;
|
|
1133
|
+
const left = await readMeter('/live/track/get/output_meter_left', t);
|
|
1134
|
+
const right = await readMeter('/live/track/get/output_meter_right', t);
|
|
1135
|
+
lines.push(`**Track ${args.track} (${name})**`);
|
|
1136
|
+
lines.push(` Left: ${meterBar(left)}`);
|
|
1137
|
+
lines.push(` Right: ${meterBar(right)}`);
|
|
1138
|
+
// Check if signal is present
|
|
1139
|
+
const avg = left >= 0 && right >= 0 ? (left + right) / 2 : Math.max(left, right);
|
|
1140
|
+
if (avg <= 0) {
|
|
1141
|
+
lines.push('');
|
|
1142
|
+
lines.push('No signal detected. Check: is the track armed? Is transport playing? Does the track have clips?');
|
|
1143
|
+
}
|
|
1144
|
+
else if (avg > 1.0) {
|
|
1145
|
+
lines.push('');
|
|
1146
|
+
lines.push('**Warning**: Signal is clipping! Reduce track volume.');
|
|
1147
|
+
}
|
|
1148
|
+
lines.push('');
|
|
1149
|
+
}
|
|
1150
|
+
// Always show master output
|
|
1151
|
+
const masterLeft = await readMeter('/live/master/get/output_meter_left');
|
|
1152
|
+
const masterRight = await readMeter('/live/master/get/output_meter_right');
|
|
1153
|
+
lines.push('**Master Output**');
|
|
1154
|
+
lines.push(` Left: ${meterBar(masterLeft)}`);
|
|
1155
|
+
lines.push(` Right: ${meterBar(masterRight)}`);
|
|
1156
|
+
const masterAvg = masterLeft >= 0 && masterRight >= 0 ? (masterLeft + masterRight) / 2 : Math.max(masterLeft, masterRight);
|
|
1157
|
+
if (masterAvg <= 0) {
|
|
1158
|
+
lines.push('');
|
|
1159
|
+
lines.push('No audio on master output. Is transport playing?');
|
|
1160
|
+
}
|
|
1161
|
+
else if (masterAvg > 0.9) {
|
|
1162
|
+
lines.push('');
|
|
1163
|
+
lines.push('**Loud!** Master is near clipping. Consider reducing levels.');
|
|
1164
|
+
}
|
|
1165
|
+
return lines.join('\n');
|
|
1166
|
+
}
|
|
1167
|
+
catch (err) {
|
|
1168
|
+
return `Ableton connection failed: ${err.message}\n\n${formatAbletonError()}`;
|
|
1169
|
+
}
|
|
1170
|
+
},
|
|
1171
|
+
});
|
|
1038
1172
|
}
|
|
1039
1173
|
//# sourceMappingURL=ableton.js.map
|
package/dist/tools/audit.js
CHANGED
|
@@ -415,7 +415,7 @@ function formatAuditReport(result) {
|
|
|
415
415
|
// Badge
|
|
416
416
|
const badgeColor = pct >= 80 ? 'brightgreen' : pct >= 60 ? 'yellow' : 'red';
|
|
417
417
|
const badgeUrl = `https://img.shields.io/badge/kbot_audit-${result.grade}_(${pct}%25)-${badgeColor}`;
|
|
418
|
-
lines.push('---', '', '### Add this badge to your README', '', '```markdown', `[](https://www.npmjs.com/package/@kernel.chat/kbot)`, '```', '', `*Audited by [kbot](https://www.npmjs.com/package/@kernel.chat/kbot) — 35 specialist agents,
|
|
418
|
+
lines.push('---', '', '### Add this badge to your README', '', '```markdown', `[](https://www.npmjs.com/package/@kernel.chat/kbot)`, '```', '', `*Audited by [kbot](https://www.npmjs.com/package/@kernel.chat/kbot) — 35 specialist agents, 787+ tools, 20 AI providers*`, `*Install: \`npm install -g @kernel.chat/kbot\` | Audit any repo: \`kbot audit owner/repo\`*`);
|
|
419
419
|
return lines.join('\n');
|
|
420
420
|
}
|
|
421
421
|
/** Generate a compact one-line summary for social sharing */
|
|
@@ -546,7 +546,7 @@ function formatAuditTerminal(result) {
|
|
|
546
546
|
// Install CTA
|
|
547
547
|
lines.push(chalk.hex(DIM)(' Audited by ') +
|
|
548
548
|
chalk.hex(VIOLET).bold('kbot') +
|
|
549
|
-
chalk.hex(DIM)(' \u2014 35 specialist agents,
|
|
549
|
+
chalk.hex(DIM)(' \u2014 35 specialist agents, 787+ tools, 20 AI providers'));
|
|
550
550
|
lines.push(chalk.hex(DIM)(' Install: ') +
|
|
551
551
|
chalk.hex(WHITE)('npm install -g @kernel.chat/kbot') +
|
|
552
552
|
chalk.hex(DIM)(' | Audit any repo: ') +
|